// Copyright (c) 2011-2019 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <qt/guiutil.h>

#include <cashaddrenc.h>
#include <chainparams.h>
#include <common/args.h>
#include <interfaces/node.h>
#include <key_io.h>
#include <logging.h>
#include <policy/policy.h>
#include <primitives/transaction.h>
#include <protocol.h>
#include <qt/bitcoinaddressvalidator.h>
#include <qt/bitcoinunits.h>
#include <qt/qvalidatedlineedit.h>
#include <qt/sendcoinsrecipient.h>
#include <script/script.h>
#include <script/standard.h>
#include <util/chaintype.h>
#include <util/exception.h>
#include <util/fs.h>
#include <util/fs_helpers.h>
#include <util/strencodings.h>
#include <util/time.h>

#ifdef WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <shellapi.h>
#include <shlobj.h>
#include <shlwapi.h>
#endif

#include <QAbstractItemView>
#include <QApplication>
#include <QClipboard>
#include <QDateTime>
#include <QDesktopServices>
#include <QDialog>
#include <QDoubleValidator>
#include <QFileDialog>
#include <QFont>
#include <QFontDatabase>
#include <QFontMetrics>
#include <QGuiApplication>
#include <QKeyEvent>
#include <QKeySequence>
#include <QLineEdit>
#include <QList>
#include <QMenu>
#include <QMouseEvent>
#include <QProcess>
#include <QProgressDialog>
#include <QRegularExpression>
#include <QScreen>
#include <QSettings>
#include <QShortcut>
#include <QSize>
#include <QStandardPaths>
#include <QString>
#include <QTextDocument> // for Qt::mightBeRichText
#include <QThread>
#include <QUrlQuery>
#include <QtGlobal>

#include <chrono>
#include <exception>
#include <fstream>
#include <string>
#include <vector>

#if defined(Q_OS_MAC)

void ForceActivation();
#endif

namespace GUIUtil {

QString dateTimeStr(const QDateTime &date) {
    return date.date().toString(QLocale().dateFormat(QLocale::ShortFormat)) +
           QString(" ") + date.toString("hh:mm");
}

QString dateTimeStr(qint64 nTime) {
    return dateTimeStr(QDateTime::fromSecsSinceEpoch(nTime));
}

QFont fixedPitchFont() {
    return QFontDatabase::systemFont(QFontDatabase::FixedFont);
}

static std::string MakeAddrInvalid(std::string addr,
                                   const CChainParams &params) {
    if (addr.size() < 2) {
        return "";
    }

    // Checksum is at the end of the address. Swapping chars to make it invalid.
    std::swap(addr[addr.size() - 1], addr[addr.size() - 2]);
    if (!IsValidDestinationString(addr, params)) {
        return addr;
    }

    return "";
}

std::string DummyAddress(const CChainParams &params) {
    // Just some dummy data to generate a convincing random-looking (but
    // consistent) address
    static const std::vector<uint8_t> dummydata = {
        0xeb, 0x15, 0x23, 0x1d, 0xfc, 0xeb, 0x60, 0x92, 0x58, 0x86,
        0xb6, 0x7d, 0x06, 0x52, 0x99, 0x92, 0x59, 0x15, 0xae, 0xb1};

    const CTxDestination dstKey = PKHash(uint160(dummydata));
    return MakeAddrInvalid(EncodeCashAddr(dstKey, params), params);
}

// Addresses are stored in the database with the encoding that the client was
// configured with at the time of creation.
//
// This converts to cashaddr.
QString convertToCashAddr(const CChainParams &params, const QString &addr) {
    if (!IsValidDestinationString(addr.toStdString(), params)) {
        // We have something sketchy as input. Do not try to convert.
        return addr;
    }
    CTxDestination dst = DecodeDestination(addr.toStdString(), params);
    return QString::fromStdString(EncodeCashAddr(dst, params));
}

void setupAddressWidget(QValidatedLineEdit *widget, QWidget *parent) {
    parent->setFocusProxy(widget);

    widget->setFont(fixedPitchFont());
    // We don't want translators to use own addresses in translations
    // and this is the only place, where this address is supplied.
    widget->setPlaceholderText(
        QObject::tr("Enter a Bitcoin address (e.g. %1)")
            .arg(QString::fromStdString(DummyAddress(Params()))));
    widget->setValidator(
        new BitcoinAddressEntryValidator(Params().CashAddrPrefix(), parent));
    widget->setCheckValidator(new BitcoinAddressCheckValidator(parent));
}

bool parseBitcoinURI(const QString &scheme, const QUrl &uri,
                     SendCoinsRecipient *out) {
    // return if URI has wrong scheme.
    if (!uri.isValid() || uri.scheme() != scheme) {
        return false;
    }

    SendCoinsRecipient rv;
    rv.address = uri.scheme() + ":" + uri.path();

    // Trim any following forward slash which may have been added by the OS
    if (rv.address.endsWith("/")) {
        rv.address.truncate(rv.address.length() - 1);
    }
    rv.amount = Amount::zero();

    QUrlQuery uriQuery(uri);
    QList<QPair<QString, QString>> items = uriQuery.queryItems();
    for (QList<QPair<QString, QString>>::iterator i = items.begin();
         i != items.end(); i++) {
        bool fShouldReturnFalse = false;
        if (i->first.startsWith("req-")) {
            i->first.remove(0, 4);
            fShouldReturnFalse = true;
        }

        if (i->first == "label") {
            rv.label = i->second;
            fShouldReturnFalse = false;
        }
        if (i->first == "message") {
            rv.message = i->second;
            fShouldReturnFalse = false;
        } else if (i->first == "amount") {
            if (!i->second.isEmpty()) {
                if (!BitcoinUnits::parse(BitcoinUnits::base, i->second,
                                         &rv.amount)) {
                    return false;
                }
            }
            fShouldReturnFalse = false;
        }

        if (fShouldReturnFalse) {
            return false;
        }
    }
    if (out) {
        *out = rv;
    }
    return true;
}

bool parseBitcoinURI(const QString &scheme, QString uri,
                     SendCoinsRecipient *out) {
    //
    //    Cannot handle this later, because bitcoincash://
    //    will cause Qt to see the part after // as host,
    //    which will lower-case it (and thus invalidate the address).
    if (uri.startsWith(scheme + "://", Qt::CaseInsensitive)) {
        uri.replace(0, scheme.length() + 3, scheme + ":");
    }
    QUrl uriInstance(uri);
    return parseBitcoinURI(scheme, uriInstance, out);
}

QString formatBitcoinURI(const SendCoinsRecipient &info) {
    return formatBitcoinURI(Params(), info);
}

QString formatBitcoinURI(const CChainParams &params,
                         const SendCoinsRecipient &info) {
    QString ret = convertToCashAddr(params, info.address);
    int paramCount = 0;

    if (info.amount != Amount::zero()) {
        ret += QString("?amount=%1")
                   .arg(BitcoinUnits::format(
                       BitcoinUnits::base, info.amount, false,
                       BitcoinUnits::SeparatorStyle::NEVER));
        paramCount++;
    }

    if (!info.label.isEmpty()) {
        QString lbl(QUrl::toPercentEncoding(info.label));
        ret += QString("%1label=%2").arg(paramCount == 0 ? "?" : "&").arg(lbl);
        paramCount++;
    }

    if (!info.message.isEmpty()) {
        QString msg(QUrl::toPercentEncoding(info.message));
        ret +=
            QString("%1message=%2").arg(paramCount == 0 ? "?" : "&").arg(msg);
        paramCount++;
    }

    return ret;
}

bool isDust(interfaces::Node &node, const QString &address, const Amount amount,
            const CChainParams &chainParams) {
    CTxDestination dest = DecodeDestination(address.toStdString(), chainParams);
    CScript script = GetScriptForDestination(dest);
    CTxOut txOut(amount, script);
    return IsDust(txOut, node.getDustRelayFee());
}

QString HtmlEscape(const QString &str, bool fMultiLine) {
    QString escaped = str.toHtmlEscaped();
    if (fMultiLine) {
        escaped = escaped.replace("\n", "<br>\n");
    }
    return escaped;
}

QString HtmlEscape(const std::string &str, bool fMultiLine) {
    return HtmlEscape(QString::fromStdString(str), fMultiLine);
}

void copyEntryData(const QAbstractItemView *view, int column, int role) {
    if (!view || !view->selectionModel()) {
        return;
    }
    QModelIndexList selection = view->selectionModel()->selectedRows(column);

    if (!selection.isEmpty()) {
        // Copy first item
        setClipboard(selection.at(0).data(role).toString());
    }
}

QList<QModelIndex> getEntryData(const QAbstractItemView *view, int column) {
    if (!view || !view->selectionModel()) {
        return QList<QModelIndex>();
    }
    return view->selectionModel()->selectedRows(column);
}

bool hasEntryData(const QAbstractItemView *view, int column, int role) {
    QModelIndexList selection = getEntryData(view, column);
    if (selection.isEmpty()) {
        return false;
    }
    return !selection.at(0).data(role).toString().isEmpty();
}

QString getDefaultDataDirectory() {
    return boostPathToQString(GetDefaultDataDir());
}

QString ExtractFirstSuffixFromFilter(const QString &filter) {
    QRegularExpression filter_re(QStringLiteral(".* \\(\\*\\.(.*)[ \\)]"),
                                 QRegularExpression::InvertedGreedinessOption);
    QString suffix;
    QRegularExpressionMatch m = filter_re.match(filter);
    if (m.hasMatch()) {
        suffix = m.captured(1);
    }
    return suffix;
}

QString getSaveFileName(QWidget *parent, const QString &caption,
                        const QString &dir, const QString &filter,
                        QString *selectedSuffixOut) {
    QString selectedFilter;
    QString myDir;
    // Default to user documents location
    if (dir.isEmpty()) {
        myDir =
            QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
    } else {
        myDir = dir;
    }
    /* Directly convert path to native OS path separators */
    QString result = QDir::toNativeSeparators(QFileDialog::getSaveFileName(
        parent, caption, myDir, filter, &selectedFilter));

    QString selectedSuffix = ExtractFirstSuffixFromFilter(selectedFilter);

    /* Add suffix if needed */
    QFileInfo info(result);
    if (!result.isEmpty()) {
        if (info.suffix().isEmpty() && !selectedSuffix.isEmpty()) {
            /* No suffix specified, add selected suffix */
            if (!result.endsWith(".")) {
                result.append(".");
            }
            result.append(selectedSuffix);
        }
    }

    /* Return selected suffix if asked to */
    if (selectedSuffixOut) {
        *selectedSuffixOut = selectedSuffix;
    }
    return result;
}

QString getOpenFileName(QWidget *parent, const QString &caption,
                        const QString &dir, const QString &filter,
                        QString *selectedSuffixOut) {
    QString selectedFilter;
    QString myDir;
    // Default to user documents location
    if (dir.isEmpty()) {
        myDir =
            QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
    } else {
        myDir = dir;
    }
    /* Directly convert path to native OS path separators */
    QString result = QDir::toNativeSeparators(QFileDialog::getOpenFileName(
        parent, caption, myDir, filter, &selectedFilter));

    if (selectedSuffixOut) {
        *selectedSuffixOut = ExtractFirstSuffixFromFilter(selectedFilter);
    }
    return result;
}

Qt::ConnectionType blockingGUIThreadConnection() {
    if (QThread::currentThread() != qApp->thread()) {
        return Qt::BlockingQueuedConnection;
    } else {
        return Qt::DirectConnection;
    }
}

bool checkPoint(const QPoint &p, const QWidget *w) {
    QWidget *atW = QApplication::widgetAt(w->mapToGlobal(p));
    if (!atW) {
        return false;
    }
    return atW->window() == w;
}

bool isObscured(QWidget *w) {
    return !(checkPoint(QPoint(0, 0), w) &&
             checkPoint(QPoint(w->width() - 1, 0), w) &&
             checkPoint(QPoint(0, w->height() - 1), w) &&
             checkPoint(QPoint(w->width() - 1, w->height() - 1), w) &&
             checkPoint(QPoint(w->width() / 2, w->height() / 2), w));
}

void bringToFront(QWidget *w) {
#ifdef Q_OS_MAC
    ForceActivation();
#endif

    if (w) {
        // activateWindow() (sometimes) helps with keyboard focus on Windows
        if (w->isMinimized()) {
            w->showNormal();
        } else {
            w->show();
        }
        w->activateWindow();
        w->raise();
    }
}

void handleCloseWindowShortcut(QWidget *w) {
    QObject::connect(new QShortcut(QKeySequence(QObject::tr("Ctrl+W")), w),
                     &QShortcut::activated, w, &QWidget::close);
}

void openDebugLogfile() {
    fs::path pathDebug = gArgs.GetDataDirNet() / "debug.log";

    /* Open debug.log with the associated application */
    if (fs::exists(pathDebug)) {
        QDesktopServices::openUrl(
            QUrl::fromLocalFile(boostPathToQString(pathDebug)));
    }
}

bool openBitcoinConf() {
    fs::path pathConfig = gArgs.GetConfigFilePath();

    /* Create the file */
    std::ofstream configFile{pathConfig, std::ios_base::app};

    if (!configFile.good()) {
        return false;
    }

    configFile.close();

    /* Open bitcoin.conf with the associated application */
    bool res = QDesktopServices::openUrl(
        QUrl::fromLocalFile(boostPathToQString(pathConfig)));
#ifdef Q_OS_MAC
    // Workaround for macOS-specific behavior; see #15409.
    if (!res) {
        res = QProcess::startDetached(
            "/usr/bin/open", QStringList{"-t", boostPathToQString(pathConfig)});
    }
#endif

    return res;
}

QStringList splitSkipEmptyParts(const QString &s, const QString &separator) {
    return s.split(separator,
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
                   Qt::SkipEmptyParts
#else
                   QString::SkipEmptyParts
#endif
    );
}

ToolTipToRichTextFilter::ToolTipToRichTextFilter(int _size_threshold,
                                                 QObject *parent)
    : QObject(parent), size_threshold(_size_threshold) {}

bool ToolTipToRichTextFilter::eventFilter(QObject *obj, QEvent *evt) {
    if (evt->type() == QEvent::ToolTipChange) {
        QWidget *widget = static_cast<QWidget *>(obj);
        QString tooltip = widget->toolTip();
        if (tooltip.size() > size_threshold && !tooltip.startsWith("<qt") &&
            !Qt::mightBeRichText(tooltip)) {
            // Envelop with <qt></qt> to make sure Qt detects this as rich text
            // Escape the current message as HTML and replace \n by <br>
            tooltip = "<qt>" + HtmlEscape(tooltip, true) + "</qt>";
            widget->setToolTip(tooltip);
            return true;
        }
    }
    return QObject::eventFilter(obj, evt);
}

LabelOutOfFocusEventFilter::LabelOutOfFocusEventFilter(QObject *parent)
    : QObject(parent) {}

bool LabelOutOfFocusEventFilter::eventFilter(QObject *watched, QEvent *event) {
    if (event->type() == QEvent::FocusOut) {
        auto focus_out = static_cast<QFocusEvent *>(event);
        if (focus_out->reason() != Qt::PopupFocusReason) {
            auto label = qobject_cast<QLabel *>(watched);
            if (label) {
                auto flags = label->textInteractionFlags();
                label->setTextInteractionFlags(Qt::NoTextInteraction);
                label->setTextInteractionFlags(flags);
            }
        }
    }

    return QObject::eventFilter(watched, event);
}

void TableViewLastColumnResizingFixer::connectViewHeadersSignals() {
    connect(tableView->horizontalHeader(), &QHeaderView::sectionResized, this,
            &TableViewLastColumnResizingFixer::on_sectionResized);
    connect(tableView->horizontalHeader(), &QHeaderView::geometriesChanged,
            this, &TableViewLastColumnResizingFixer::on_geometriesChanged);
}

// We need to disconnect these while handling the resize events, otherwise we
// can enter infinite loops.
void TableViewLastColumnResizingFixer::disconnectViewHeadersSignals() {
    disconnect(tableView->horizontalHeader(), &QHeaderView::sectionResized,
               this, &TableViewLastColumnResizingFixer::on_sectionResized);
    disconnect(tableView->horizontalHeader(), &QHeaderView::geometriesChanged,
               this, &TableViewLastColumnResizingFixer::on_geometriesChanged);
} // namespace GUIUtil

// Setup the resize mode, handles compatibility for Qt5 and below as the method
// signatures changed.
// Refactored here for readability.
void TableViewLastColumnResizingFixer::setViewHeaderResizeMode(
    int logicalIndex, QHeaderView::ResizeMode resizeMode) {
    tableView->horizontalHeader()->setSectionResizeMode(logicalIndex,
                                                        resizeMode);
}

void TableViewLastColumnResizingFixer::resizeColumn(int nColumnIndex,
                                                    int width) {
    tableView->setColumnWidth(nColumnIndex, width);
    tableView->horizontalHeader()->resizeSection(nColumnIndex, width);
}

int TableViewLastColumnResizingFixer::getColumnsWidth() {
    int nColumnsWidthSum = 0;
    for (int i = 0; i < columnCount; i++) {
        nColumnsWidthSum += tableView->horizontalHeader()->sectionSize(i);
    }
    return nColumnsWidthSum;
}

int TableViewLastColumnResizingFixer::getAvailableWidthForColumn(int column) {
    int nResult = lastColumnMinimumWidth;
    int nTableWidth = tableView->horizontalHeader()->width();

    if (nTableWidth > 0) {
        int nOtherColsWidth =
            getColumnsWidth() -
            tableView->horizontalHeader()->sectionSize(column);
        nResult = std::max(nResult, nTableWidth - nOtherColsWidth);
    }

    return nResult;
}

// Make sure we don't make the columns wider than the table's viewport width.
void TableViewLastColumnResizingFixer::adjustTableColumnsWidth() {
    disconnectViewHeadersSignals();
    resizeColumn(lastColumnIndex, getAvailableWidthForColumn(lastColumnIndex));
    connectViewHeadersSignals();

    int nTableWidth = tableView->horizontalHeader()->width();
    int nColsWidth = getColumnsWidth();
    if (nColsWidth > nTableWidth) {
        resizeColumn(secondToLastColumnIndex,
                     getAvailableWidthForColumn(secondToLastColumnIndex));
    }
}

// Make column use all the space available, useful during window resizing.
void TableViewLastColumnResizingFixer::stretchColumnWidth(int column) {
    disconnectViewHeadersSignals();
    resizeColumn(column, getAvailableWidthForColumn(column));
    connectViewHeadersSignals();
}

// When a section is resized this is a slot-proxy for ajustAmountColumnWidth().
void TableViewLastColumnResizingFixer::on_sectionResized(int logicalIndex,
                                                         int oldSize,
                                                         int newSize) {
    adjustTableColumnsWidth();
    int remainingWidth = getAvailableWidthForColumn(logicalIndex);
    if (newSize > remainingWidth) {
        resizeColumn(logicalIndex, remainingWidth);
    }
}

// When the table's geometry is ready, we manually perform the stretch of the
// "Message" column,
// as the "Stretch" resize mode does not allow for interactive resizing.
void TableViewLastColumnResizingFixer::on_geometriesChanged() {
    if ((getColumnsWidth() - this->tableView->horizontalHeader()->width()) !=
        0) {
        disconnectViewHeadersSignals();
        resizeColumn(secondToLastColumnIndex,
                     getAvailableWidthForColumn(secondToLastColumnIndex));
        connectViewHeadersSignals();
    }
}

/**
 * Initializes all internal variables and prepares the
 * the resize modes of the last 2 columns of the table and
 */
TableViewLastColumnResizingFixer::TableViewLastColumnResizingFixer(
    QTableView *table, int lastColMinimumWidth, int allColsMinimumWidth,
    QObject *parent)
    : QObject(parent), tableView(table),
      lastColumnMinimumWidth(lastColMinimumWidth),
      allColumnsMinimumWidth(allColsMinimumWidth) {
    columnCount = tableView->horizontalHeader()->count();
    lastColumnIndex = columnCount - 1;
    secondToLastColumnIndex = columnCount - 2;
    tableView->horizontalHeader()->setMinimumSectionSize(
        allColumnsMinimumWidth);
    setViewHeaderResizeMode(secondToLastColumnIndex, QHeaderView::Interactive);
    setViewHeaderResizeMode(lastColumnIndex, QHeaderView::Interactive);
}

#ifdef WIN32
static fs::path StartupShortcutPath() {
    ChainType chain = gArgs.GetChainType();
    if (chain == ChainType::MAIN) {
        return GetSpecialFolderPath(CSIDL_STARTUP) / "Bitcoin.lnk";
    }
    // Remove this special case when CBaseChainParams::DataDir() is incremented
    // to "testnet4"
    if (chain == ChainType::TESTNET) {
        return GetSpecialFolderPath(CSIDL_STARTUP) / "Bitcoin (testnet).lnk";
    }
    return GetSpecialFolderPath(CSIDL_STARTUP) /
           strprintf("Bitcoin (%s).lnk", ChainTypeToString(chain));
}

bool GetStartOnSystemStartup() {
    // check for Bitcoin*.lnk
    return fs::exists(StartupShortcutPath());
}

bool SetStartOnSystemStartup(bool fAutoStart) {
    // If the shortcut exists already, remove it for updating
    fs::remove(StartupShortcutPath());

    if (fAutoStart) {
        CoInitialize(nullptr);

        // Get a pointer to the IShellLink interface.
        IShellLinkW *psl = nullptr;
        HRESULT hres =
            CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
                             IID_IShellLinkW, reinterpret_cast<void **>(&psl));

        if (SUCCEEDED(hres)) {
            // Get the current executable path
            WCHAR pszExePath[MAX_PATH];
            GetModuleFileNameW(nullptr, pszExePath, ARRAYSIZE(pszExePath));

            // Start client minimized
            QString strArgs = "-min";
            // Set -testnet /-regtest options
            strArgs += QString::fromStdString(
                strprintf(" -chain=%s", gArgs.GetChainTypeString()));

            // Set the path to the shortcut target
            psl->SetPath(pszExePath);
            PathRemoveFileSpecW(pszExePath);
            psl->SetWorkingDirectory(pszExePath);
            psl->SetShowCmd(SW_SHOWMINNOACTIVE);
            psl->SetArguments(strArgs.toStdWString().c_str());

            // Query IShellLink for the IPersistFile interface for
            // saving the shortcut in persistent storage.
            IPersistFile *ppf = nullptr;
            hres = psl->QueryInterface(IID_IPersistFile,
                                       reinterpret_cast<void **>(&ppf));
            if (SUCCEEDED(hres)) {
                // Save the link by calling IPersistFile::Save.
                hres = ppf->Save(StartupShortcutPath().wstring().c_str(), TRUE);
                ppf->Release();
                psl->Release();
                CoUninitialize();
                return true;
            }
            psl->Release();
        }
        CoUninitialize();
        return false;
    }
    return true;
}
#elif defined(Q_OS_LINUX)

// Follow the Desktop Application Autostart Spec:
// http://standards.freedesktop.org/autostart-spec/autostart-spec-latest.html

static fs::path GetAutostartDir() {
    char *pszConfigHome = getenv("XDG_CONFIG_HOME");
    if (pszConfigHome) {
        return fs::path(pszConfigHome) / "autostart";
    }
    char *pszHome = getenv("HOME");
    if (pszHome) {
        return fs::path(pszHome) / ".config" / "autostart";
    }
    return fs::path();
}

static fs::path GetAutostartFilePath() {
    ChainType chain = gArgs.GetChainType();
    if (chain == ChainType::MAIN) {
        return GetAutostartDir() / "bitcoin.desktop";
    }
    return GetAutostartDir() /
           strprintf("bitcoin-%s.desktop", ChainTypeToString(chain));
}

bool GetStartOnSystemStartup() {
    std::ifstream optionFile{GetAutostartFilePath()};
    if (!optionFile.good()) {
        return false;
    }
    // Scan through file for "Hidden=true":
    std::string line;
    while (!optionFile.eof()) {
        getline(optionFile, line);
        if (line.find("Hidden") != std::string::npos &&
            line.find("true") != std::string::npos) {
            return false;
        }
    }
    optionFile.close();

    return true;
}

bool SetStartOnSystemStartup(bool fAutoStart) {
    if (!fAutoStart) {
        fs::remove(GetAutostartFilePath());
    } else {
        char pszExePath[MAX_PATH + 1];
        ssize_t r =
            readlink("/proc/self/exe", pszExePath, sizeof(pszExePath) - 1);
        if (r == -1) {
            return false;
        }
        pszExePath[r] = '\0';

        fs::create_directories(GetAutostartDir());

        std::ofstream optionFile{GetAutostartFilePath(),
                                 std::ios_base::out | std::ios_base::trunc};
        if (!optionFile.good()) {
            return false;
        }
        ChainType chain = gArgs.GetChainType();
        // Write a bitcoin.desktop file to the autostart directory:
        optionFile << "[Desktop Entry]\n";
        optionFile << "Type=Application\n";
        if (chain == ChainType::MAIN) {
            optionFile << "Name=Bitcoin\n";
        } else {
            optionFile << strprintf("Name=Bitcoin (%s)\n",
                                    ChainTypeToString(chain));
        }
        optionFile << "Exec=" << pszExePath
                   << strprintf(" -min -chain=%s\n", ChainTypeToString(chain));
        optionFile << "Terminal=false\n";
        optionFile << "Hidden=false\n";
        optionFile.close();
    }
    return true;
}

#else

bool GetStartOnSystemStartup() {
    return false;
}
bool SetStartOnSystemStartup(bool fAutoStart) {
    return false;
}

#endif

void setClipboard(const QString &str) {
    QApplication::clipboard()->setText(str, QClipboard::Clipboard);
    QApplication::clipboard()->setText(str, QClipboard::Selection);
}

fs::path qstringToBoostPath(const QString &path) {
    return fs::u8path(path.toStdString());
}

QString boostPathToQString(const fs::path &path) {
    return QString::fromStdString(path.u8string());
}

QString NetworkToQString(Network net) {
    switch (net) {
        case NET_UNROUTABLE:
            return QObject::tr("Unroutable");
        case NET_IPV4:
            return "IPv4";
        case NET_IPV6:
            return "IPv6";
        case NET_ONION:
            return "Onion";
        case NET_I2P:
            return "I2P";
        case NET_CJDNS:
            return "CJDNS";
        case NET_INTERNAL:
            return QObject::tr("Internal");
        case NET_MAX:
            assert(false);
    } // no default case, so the compiler can warn about missing cases
    assert(false);
}

QString formatDurationStr(std::chrono::seconds dur) {
    const auto secs = count_seconds(dur);
    QStringList strList;
    int days = secs / 86400;
    int hours = (secs % 86400) / 3600;
    int mins = (secs % 3600) / 60;
    int seconds = secs % 60;

    if (days) {
        strList.append(QString(QObject::tr("%1 d")).arg(days));
    }
    if (hours) {
        strList.append(QString(QObject::tr("%1 h")).arg(hours));
    }
    if (mins) {
        strList.append(QString(QObject::tr("%1 m")).arg(mins));
    }
    if (seconds || (!days && !hours && !mins)) {
        strList.append(QString(QObject::tr("%1 s")).arg(seconds));
    }

    return strList.join(" ");
}

QString formatServicesStr(quint64 mask) {
    QStringList strList;

    constexpr uint64_t nonExperimentalMask =
        (NODE_LAST_NON_EXPERIMENTAL_SERVICE_BIT << 1) - 1;
    for (const auto &flag : serviceFlagsToStr(mask & nonExperimentalMask)) {
        strList.append(QString::fromStdString(flag));
    }

    if (strList.size()) {
        return strList.join(" & ");
    } else {
        return QObject::tr("None");
    }
}

QString formatPingTime(std::chrono::microseconds ping_time) {
    return (ping_time == std::chrono::microseconds::max() || ping_time == 0us)
               ? QObject::tr("N/A")
               : QString(QObject::tr("%1 ms"))
                     .arg(QString::number(
                         int(count_microseconds(ping_time) / 1000), 10));
}

QString formatTimeOffset(int64_t nTimeOffset) {
    return QString(QObject::tr("%1 s"))
        .arg(QString::number((int)nTimeOffset, 10));
}

QString formatNiceTimeOffset(qint64 secs) {
    // Represent time from last generated block in human readable text
    QString timeBehindText;
    const int HOUR_IN_SECONDS = 60 * 60;
    const int DAY_IN_SECONDS = 24 * 60 * 60;
    const int WEEK_IN_SECONDS = 7 * 24 * 60 * 60;
    // Average length of year in Gregorian calendar
    const int YEAR_IN_SECONDS = 31556952;
    if (secs < 60) {
        timeBehindText = QObject::tr("%n second(s)", "", secs);
    } else if (secs < 2 * HOUR_IN_SECONDS) {
        timeBehindText = QObject::tr("%n minute(s)", "", secs / 60);
    } else if (secs < 2 * DAY_IN_SECONDS) {
        timeBehindText = QObject::tr("%n hour(s)", "", secs / HOUR_IN_SECONDS);
    } else if (secs < 2 * WEEK_IN_SECONDS) {
        timeBehindText = QObject::tr("%n day(s)", "", secs / DAY_IN_SECONDS);
    } else if (secs < YEAR_IN_SECONDS) {
        timeBehindText = QObject::tr("%n week(s)", "", secs / WEEK_IN_SECONDS);
    } else {
        qint64 years = secs / YEAR_IN_SECONDS;
        qint64 remainder = secs % YEAR_IN_SECONDS;
        timeBehindText = QObject::tr("%1 and %2")
                             .arg(QObject::tr("%n year(s)", "", years))
                             .arg(QObject::tr("%n week(s)", "",
                                              remainder / WEEK_IN_SECONDS));
    }
    return timeBehindText;
}

QString formatBytes(uint64_t bytes) {
    if (bytes < 1024) {
        return QString(QObject::tr("%1 B")).arg(bytes);
    }
    if (bytes < 1024 * 1024) {
        return QString(QObject::tr("%1 KB")).arg(bytes / 1024);
    }
    if (bytes < 1024 * 1024 * 1024) {
        return QString(QObject::tr("%1 MB")).arg(bytes / 1024 / 1024);
    }

    return QString(QObject::tr("%1 GB")).arg(bytes / 1024 / 1024 / 1024);
}

bool ClickableLabel::hasPixmap() const {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
    return !pixmap(Qt::ReturnByValue).isNull();
#else
    return pixmap() != nullptr;
#endif
}

qreal calculateIdealFontSize(int width, const QString &text, QFont font,
                             qreal minPointSize, qreal font_size) {
    while (font_size >= minPointSize) {
        font.setPointSizeF(font_size);
        QFontMetrics fm(font);
        if (GUIUtil::TextWidth(fm, text) < width) {
            break;
        }
        font_size -= 0.5;
    }
    return font_size;
}

void ClickableLabel::mouseReleaseEvent(QMouseEvent *event) {
    Q_EMIT clicked(event->pos());
}

void ClickableProgressBar::mouseReleaseEvent(QMouseEvent *event) {
    Q_EMIT clicked(event->pos());
}

bool ItemDelegate::eventFilter(QObject *object, QEvent *event) {
    if (event->type() == QEvent::KeyPress) {
        if (static_cast<QKeyEvent *>(event)->key() == Qt::Key_Escape) {
            Q_EMIT keyEscapePressed();
        }
    }
    return QItemDelegate::eventFilter(object, event);
}

int TextWidth(const QFontMetrics &fm, const QString &text) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
    return fm.horizontalAdvance(text);
#else
    return fm.width(text);
#endif
}

void PolishProgressDialog(QProgressDialog *dialog) {
#ifdef Q_OS_MAC
    // Workaround for macOS-only Qt bug; see: QTBUG-65750, QTBUG-70357.
    const int margin = GUIUtil::TextWidth(dialog->fontMetrics(), "X");
    dialog->resize(dialog->width() + 2 * margin, dialog->height());
    dialog->show();
#else
    Q_UNUSED(dialog);
#endif
}

void LogQtInfo() {
#ifdef QT_STATIC
    const std::string qt_link{"static"};
#else
    const std::string qt_link{"dynamic"};
#endif
#ifdef QT_STATICPLUGIN
    const std::string plugin_link{"static"};
#else
    const std::string plugin_link{"dynamic"};
#endif
    LogPrintf("Qt %s (%s), plugin=%s (%s)\n", qVersion(), qt_link,
              QGuiApplication::platformName().toStdString(), plugin_link);
    LogPrintf("System: %s, %s\n", QSysInfo::prettyProductName().toStdString(),
              QSysInfo::buildAbi().toStdString());
    for (const QScreen *s : QGuiApplication::screens()) {
        LogPrintf("Screen: %s %dx%d, pixel ratio=%.1f\n",
                  s->name().toStdString(), s->size().width(),
                  s->size().height(), s->devicePixelRatio());
    }
}

void PopupMenu(QMenu *menu, const QPoint &point, QAction *at_action) {
    // The qminimal plugin does not provide window system integration.
    if (QApplication::platformName() == "minimal") {
        return;
    }
    menu->popup(point, at_action);
}

void ShowModalDialogAsynchronously(QDialog *dialog) {
    dialog->setAttribute(Qt::WA_DeleteOnClose);
    dialog->setWindowModality(Qt::ApplicationModal);
    dialog->show();
}

} // namespace GUIUtil
